#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<errno.h>
#include<stdbool.h>
enum Redir
{
    No,
    In_Redir,
    Out_Redir,
    App_Redir
};
#define SIZE 512
#define Seq " "
#define N 32
#define Split(p) do{p += strlen(p) - 1;  while(*p != '/') p--; p++;}while(0)
#define SkipSpace(cmd, pos) do{while(cmd[pos] == ' ')pos++;}while(0)

//文件输入输出重定向相关的全局参数：
char* filename = NULL;
enum Redir redir_flag = No;
int save_stdout = 1;
int save_stdin = 0;

//相关函数的声明;
extern int putenv(char *string);

//命令行参数表：
char* argv[N];
//cwd数组维护当前的PWD值, owd则维护之前的：
char owd[SIZE];
char cwd[SIZE];
int lastcode = 0;

const char* getUser()
{
    char* str = getenv("USER");
    return str;
}
const char* getHome()
{
    char* str = getenv("HOME");
    return str;
}
const char* getHostname()
{
    char* str = getenv("HOSTNAME");
    return str;
}
const char* getCpwd()
{
    char* str = getenv("PWD");
    return str;
}
//打开文件和释放文件资源：
int openFile()
{
    int fd = -1;
    if(filename != NULL)
    {
        save_stdin = dup(0);
        save_stdout = dup(1);
        if(redir_flag == In_Redir)
        {
            fd = open(filename, O_RDONLY);
            if(fd == -1)
            {
                printf("stdin error->%s\n", filename);
                exit(-1);
            }
            fd = dup2(fd, 0);
        }
        else if(redir_flag == Out_Redir)
        {
            fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
            fd = dup2(fd, 1);
        }
        else if(redir_flag == App_Redir)
        {
            fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
            fd = dup2(fd, 1); 
        }
    }
    return fd;
}
/*函数两个作用：*/
//1.校准标准输入输出；
//2.filename字符串置空，这很重要，filename不置空，可能会导致openFile被异常执行！
void freeFile(int fd)
{
    //发生了重定向就走这个逻辑：
    if(fd != -1)
    {
        //这里一定要小心，如果你不该进来的时候进来了，比如你的进程没有重定向操作，按理来说就必要进来，但是你进来了，这带来的不仅仅是效率问题更加严重的是标准输入输出被异常关闭了：
        //校准标准输入：
        dup2(save_stdin,  0);
        close(save_stdin);
        save_stdin = 0;
        //校准标准输出：
        dup2(save_stdout, 1);
        close(save_stdout);
        save_stdout = 1;
    }
    //发生了checkRedir事件，就走这个逻辑：
    //最后filename置空：
    filename = NULL;
}
void makeCommandAndPrint()
{
    char line[SIZE];
    const char* path = getCpwd();
    Split(path);
    snprintf(line, SIZE, "[%s@%s %s]>", getUser(), getHostname(), path);
    printf("%s", line);
    fflush(stdout);
}
int getUserCommand(char* user, size_t size)
{
    //从标准输入流stdin，即键盘将命令字符读取进来，并存储到user字符数组里面：
    fgets(user, size, stdin);
    int ret = strlen(user);
    user[ret-1] = '\0';
    return ret - 1;
}
void splitCommandLine(char* line)
{
    argv[0] = strtok(line, Seq);
    int index = 1;
    while((argv[index++] = strtok(NULL, Seq)));
}
void checkRedir(char *cmd)
{
    //ls -la > log.txt
    //cat < log.txt
    size_t pos = 0;
    while(pos < strlen(cmd))
    {
        //>，>>，<：
        if(cmd[pos] == '>')
        {
            cmd[pos] = 0;
            if(cmd[pos+1] == '>')
            {
                redir_flag = App_Redir;
                pos += 2;
            }
            else 
            {
                redir_flag = Out_Redir;
                pos++;
            }
            SkipSpace(cmd, pos);
            filename = cmd + pos;
        }
        else if(cmd[pos] == '<')
        {
            redir_flag = In_Redir;
            cmd[pos] = 0;
            pos++;
            SkipSpace(cmd, pos);
            filename = cmd + pos;
        }
        else pos++;
    }
}
void executeCommand(bool signal)
{
    pid_t id = fork();
    if(id == 0)
    {
        //child:
        openFile();
        execvp(argv[0], argv);
        exit(errno);
    }
    else 
    {
       //father:
       //细节：checkRedir之后，若不处理filename，这可能导致文件的重复打开：
       freeFile(signal == true ? 0 : -1);
       int status = 0;
       pid_t wid = waitpid(id, &status, 0);
       if(wid > 0)
       {
           if(WIFEXITED(status))
           {
               int tmp = WEXITSTATUS(status);
               if(tmp)
               {
                   printf("%s: errno[%d]->%s\n", argv[0], tmp, strerror(tmp));
               }
               lastcode = tmp;
           }
           else 
           {
               printf("The command abnormally exit, exit_signal: %d\n", status & 0x7f);
           }
       }
       else 
       {
           printf("ERRNO：Can't create child process or the child's pid doesn't exist\n");
           exit(1);
       }
    }
}
int echo()
{
    int ret = 0;
    if(strcmp(argv[1], "$?") == 0)printf("%d\n", lastcode);
    else
    {
        int pos = 1;
        while(argv[pos])
        {
            printf("%s", argv[pos]);
            pos++;
        }
    }
    return ret;
}
int cd()
{
    //ret用于记录cd函数是否执行成功：ret == 0表示成功执行, !0表示有错误;
    int ret = 0;
    //更新当前父进程的工作目录：
    char *path = argv[1];
    if(path == NULL || strcmp(path, "~") == 0)
    {
        path = (char*)getHome();
    }
    else if(strcmp(path, "-") == 0)
    {
        if(strlen(owd) == 0)
        {
            ret = 1;
            printf("cd: OLDPWD not set");
            goto kk;
        }
        //拿到之前旧的路径owd：
        strcpy(path, owd);
    }
    //先保存之前旧的路径owd：
    strcpy(owd, getCpwd());
    //更新当前的工作路径：
    int tmp = chdir(path);
    if(tmp == -1)
    {
        ret = errno;
        goto kk;
    }
    //更新当前系统的PWD环境变量：
    char temp[SIZE];
    getcwd(temp, SIZE);
    snprintf(cwd, SIZE, "PWD=%s", temp);
    putenv(cwd);
kk:
    return ret;
}
bool checkBuildin()
{
    //用这个函数进行重定向：
    openFile();
    bool ret = false;
    if(strcmp(argv[0], "cd") == 0)
    {
        ret = true;
        lastcode = cd();
    }
    if(strcmp(argv[0], "echo") == 0)
    {
        ret = true;
        lastcode = echo();
    }
    fflush(stdout);
    return ret;
}
int main()
{
    system("stty erase ^H");
    while(1)
    {
        //1.我们需要自己输出一个命令行：
        makeCommandAndPrint();
        //2.获取用户的命令行字符串：注意如果编译器啥也没读到，我们后面工作就不做了
        char usercommand[SIZE];
        int n = getUserCommand(usercommand, SIZE);
        if(n == 0) continue;

        //扩展一->检查重定向：
        checkRedir(usercommand);
        bool signal = filename == NULL ? false : true;

        //debug:
        //printf("cmd: %s\n", usercommand);
        //printf("redir: %d\n", redir_flag);
        //printf("filename: %s\n", filename);
        
        //3.对命令行字符串进行分割，并存储到命令行参数表：
        splitCommandLine(usercommand);
        //4.检测命令是否是内建命令，如果是内建命令，就不再执行后续的程序了：
        if(checkBuildin()) 
        {
            //4.1若内建命令执行了，就需要重定向标准输入输出，同时filename置空：
            freeFile(0);
            continue;  
        }
        //4.2反之，不能freeFile！！！，不能影响到我们后面代码的执行：
        //5.创建子进程，执行用户的命令：
        executeCommand(signal);
    }
    return 0;
}
